package spring.data.jpa.repository.factory;

import java.io.Serializable;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.Optional;

import javax.persistence.EmbeddedId;
import javax.persistence.Id;

import org.springframework.data.repository.core.support.AbstractRepositoryMetadata;
import org.springframework.util.Assert;

import spring.data.jpa.repository.CommonJpaRepository;

public class DomainRepositoryMetadata extends AbstractRepositoryMetadata {

    private final Class<? extends Serializable> idType;
    private final Class<?> domainType;

    /**
     * Creates a new {@link DomainRepositoryMetadata} for the given repository
     * interface.
     * 
     * @param domainClass
     *            must not be {@literal null}.
     */
    public DomainRepositoryMetadata(Class<?> domainClass) {

        super(CommonJpaRepository.class);
        Assert.notNull(domainClass, "DomainClass must not be null");
        this.idType = resolveIdType(domainClass);
        this.domainType = domainClass;
    }

    /*
     * (non-Javadoc)
     * 
     * @see
     * org.springframework.data.repository.core.RepositoryMetadata#getDomainType
     * ()
     */
    @Override
    public Class<?> getDomainType() {
        return this.domainType;
    }

    /*
     * (non-Javadoc)
     * 
     * @see
     * org.springframework.data.repository.core.RepositoryMetadata#getIdType()
     */
    @Override
    public Class<? extends Serializable> getIdType() {
        return this.idType;
    }

    @Override
    public boolean isPagingRepository() {

        return true;
    }

    @SuppressWarnings("unchecked")
    public Optional<Class<? extends Serializable>> getDeclaredField(Class<?> domainClass) {
        if (domainClass == Object.class) {
            return Optional.empty();
        }
        Field[] fields = domainClass.getDeclaredFields();
        for (Field field : fields) {
            Id classId = field.getAnnotation(Id.class);
            EmbeddedId classEmbeddedId = field.getAnnotation(EmbeddedId.class);
            if (classId != null || classEmbeddedId != null) {
                return Optional.of((Class<? extends Serializable>) field.getType());
            }
        }
        return getDeclaredField(domainClass.getSuperclass());
    }

    @SuppressWarnings("unchecked")
    public Optional<Class<? extends Serializable>> getDeclaredMethod(Class<?> domainClass) {
        if (domainClass == Object.class) {
            return Optional.empty();
        }
        Method[] methods = domainClass.getDeclaredMethods();
        for (Method method : methods) {
            Id classId = method.getAnnotation(Id.class);
            EmbeddedId classEmbeddedId = method.getAnnotation(EmbeddedId.class);
            if (classId != null || classEmbeddedId != null) {
                return Optional.of((Class<? extends Serializable>) method.getReturnType());
            }
        }
        return getDeclaredMethod(domainClass.getSuperclass());
    }

    private Class<? extends Serializable> resolveIdType(Class<?> domainClass) {
        Class<? extends Serializable> clazz = getDeclaredField(domainClass)
                .orElse(getDeclaredMethod(domainClass).orElse(null));
        if (clazz == null) {
            throw new IllegalArgumentException(String.format("Could not resolve id type of DomainClass: %s!",
                    domainClass));
        }
        return clazz;
    }

}
